/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package locusts.common.entities;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.ListModel;
import javax.swing.event.EventListenerList;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

/**
 * A collection of EntityType objects, that extends normal functionality
 * because this list indexes are used for EntityType id lookup. Also maintains
 * a map of name strings to EntityType.
 *
 * @author Hamish Morgan
 * @see EntityType
 */
public class EntityTypeList implements Collection<EntityType>, Serializable,
        Cloneable {

    /**
     * Used to stop run-away list growth when an invalid id is supplied.
     */
    private transient static final int MAX_TYPE_ID = 100;
    //
    private List<EntityType> types;
    private transient Map<String, WeakReference<EntityType>> names;

    /**
     * Protected constructor used for dependancy injection in testing.
     * @param types
     */
    protected EntityTypeList(List<EntityType> types) {
        this.types = types;
        rebuildNames();
    }

    /**
     * Constructor that instantiates a new instance of the EntityTypeList
     * class.
     */
    public EntityTypeList() {
        this(new CopyOnWriteArrayList<EntityType>());
    }

    /**
     * Private constructor used for cloning.
     * 
     * @param that
     */
    private EntityTypeList(EntityTypeList that) {
        this();
        addAll(that.types);
    }

    private void rebuildNames() {
        if (names == null)
            names = new WeakHashMap<String, WeakReference<EntityType>>();
        for (EntityType type : types)
            names.put(type.getName(), new WeakReference<EntityType>(type));
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(types);
    }

    @SuppressWarnings("unchecked")
    private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        types = (List<EntityType>) in.readObject();
        rebuildNames();
    }

    private final void growTypesList(int id) {
        if (id < 0 || id > MAX_TYPE_ID)
            throw new IllegalArgumentException(
                    "Argument id must be between 0 and " + MAX_TYPE_ID);
        while (types.size() <= id)
            types.add(null);
    }

    public int size() {
        return types.size();
    }

    public boolean isEmpty() {
        return types.isEmpty();
    }

    public boolean contains(Object o) {
        return types.contains((EntityType) o);
    }

    public boolean contains(EntityType o) {
        return types.contains(o);
    }

    public boolean containsId(int id) {
        return types.size() > id && types.get(id) != null;
    }

    public boolean containsTypeOf(Entity e) {
        return containsId(e.getTypeId());
    }

    public boolean containsName(String name) {
        return names.containsKey(name);
    }

    public boolean containsAll(Collection<?> c) {
        return types.containsAll(c);
    }

    public boolean remove(Object o) {
        EntityType t = (EntityType) o;
        boolean result = types.remove(t);
        if(result) {
            fireIntervalRemoved(t.getId(), t.getId());
        }
        return result;
    }

    public EntityType getId(int id) {
        return id < size() ? types.get(id) : null;
    }

    public EntityType getName(String name) {
        return names.get(name).get();
    }

    public EntityType getTypeOf(Entity e) {
        return types.get(e.getTypeId());
    }

    public boolean addAll(EntityType... c) {
        int minId = Integer.MAX_VALUE;
        int maxId = -Integer.MAX_VALUE;
        boolean changed = false;
        for (EntityType t : c) {
            final int id = t.getId();
            growTypesList(id);
            if (types.get(id) == null) {
                minId = Math.min(minId, id);
                maxId = Math.max(maxId, id);
                types.set(id, t);
                names.put(t.getName(), new WeakReference<EntityType>(t));
                changed = true;
            }
        }
        if(changed) {
            fireIntervalAdded(minId, maxId);
        }
        return changed;
    }

    public boolean addAll(Collection<? extends EntityType> c) {
        int minId = Integer.MAX_VALUE;
        int maxId = -Integer.MAX_VALUE;
        boolean changed = false;
        for (EntityType t : c) {
            final int id = t.getId();
            growTypesList(id);
            if (types.get(id) == null) {
                minId = Math.min(minId, id);
                maxId = Math.max(maxId, id);
                types.set(id, t);
                names.put(t.getName(), new WeakReference<EntityType>(t));
                changed = true;
            }
        }
        if(changed) {
            fireIntervalAdded(minId, maxId);
        }
        return changed;
    }

    public boolean removeAll(Collection<?> c) {
        int minId = Integer.MAX_VALUE;
        int maxId = -Integer.MAX_VALUE;
        boolean changed = false;
        for (Object o : c) {
            final EntityType t = (EntityType) o;
            final int id = t.getId();
            growTypesList(id);
            if (types.get(id) != null) {
                minId = Math.min(minId, id);
                maxId = Math.max(maxId, id);
                types.set(id, null);
                changed = true;
            }
        }
        if(changed) {
            fireIntervalRemoved(minId, maxId);
        }
        return changed;
    }

    public boolean retainAll(Collection<?> c) {
        int minId = Integer.MAX_VALUE;
        int maxId = -Integer.MAX_VALUE;
        boolean changed = false;
        for (Object o : c) {
            final EntityType t = (EntityType) o;
            if (!c.contains(t)) {
                final int id = t.getId();
                minId = Math.min(minId, id);
                maxId = Math.max(maxId, id);
                types.set(id, null);
                changed = true;
            }
        }
        if(changed) {
            fireIntervalRemoved(minId, maxId);
        }
        return changed;
    }

    public void clear() {
        types.clear();
        fireContentsChanged(0, 100);
    }

    public boolean add(EntityType o) {
        growTypesList(o.getId());
        types.set(o.getId(), o);
        names.put(o.getName(), new WeakReference<EntityType>(o));
        fireIntervalAdded(o.getId(), o.getId());
        return true;
    }

    public boolean add(String name) {
        final EntityType newType = new EntityType(name);
        final boolean result = this.add(newType);
        return result;
    }

    public EntityType removeId(int id) {
        EntityType t = getId(id);
        if (t != null)
            types.set(id, null);
        fireContentsChanged(id, id);
        return t;
    }

    public Iterator<EntityType> iterator() {
        return types.iterator();
    }

    public Object[] toArray() {
        return types.toArray();
    }

    public <T> T[] toArray(T[] a) {
        return types.toArray(a);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        final EntityTypeList other = (EntityTypeList) obj;
        if (this.types != other.types &&
                (this.types == null || !this.types.equals(other.types)))
            return false;
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 31 * hash + (this.types != null ? this.types.hashCode() : 0);
        return hash;
    }

    @Override
    public EntityTypeList clone() {
        return new EntityTypeList(this);
    }

    @Override
    public String toString() {
        return types.toString();
    }

    // ==============================================
    // List model stuff
    //
    private final transient EventListenerList listeners = new EventListenerList();

    protected void fireIntervalAdded(int index0, int index1) {
        final ListDataEvent e = new ListDataEvent(
                this, ListDataEvent.INTERVAL_ADDED, index0, index1);
        for (ListDataListener l : listeners.getListeners(ListDataListener.class))
            l.intervalAdded(e);
    }

    protected void fireIntervalRemoved(int index0, int index1) {
        final ListDataEvent e = new ListDataEvent(this,
                ListDataEvent.INTERVAL_REMOVED, index0, index1);
        for (ListDataListener l : listeners.getListeners(ListDataListener.class))
            l.intervalRemoved(e);
    }

    protected void fireContentsChanged(int index0, int index1) {
        final ListDataEvent e = new ListDataEvent(this,
                ListDataEvent.CONTENTS_CHANGED, index0, index1);
        for (ListDataListener l : listeners.getListeners(ListDataListener.class))
            l.contentsChanged(e);
    }

    public void addListDataListener(ListDataListener listener) {
        listeners.add(ListDataListener.class, listener);
    }

    public void removeListDataListener(ListDataListener listener) {
        listeners.remove(ListDataListener.class, listener);
    }
}
